苦しんで覚えるC言語 16.2章 構造体の引数 P336 – P344

構造体で情報を渡す

構造体として定義したものは、変数と同じ扱いになります。
関数の引数として渡すこともできるので、一度にたくさんの情報を渡すことが可能となります。

構造体変数を単体で渡すと、渡したい数分だけ引数に設定する必要がありますが、構造体としてまとめてしまうことで、複数の変数情報を一つのパッケージされた構造体変数にて渡せます。
参考書のコード例を見てみましょう。

#include <stdio.h>
#include <string.h>

typedef struct{
  int year;         /* 学生 */
  int clas;         /* クラス */
  int number;       /* 出席番号 */
  char name[64];    /* 名前 */
  double stateure;  /* 身長 */
  double weight;    /* 体重 */
}student;

void student_print(student data);

int main(void)
{
    student data;

    data.year = 3;
    data.clas = 4;
    data.number = 18;
    strcpy(data.name,"MARIO");
    data.stature = 168.2;
    data.weight = 72.4;

    student_print(data);  /* 構造体引数とした関数の呼び出し */

    return 0;
}

void student_print(student data)
{
    printf("[学生]:%d\n", data.year);
    printf("[クラス]:%d\n", data.clas);
    printf("[出席番号]:%d\n", data.number);
    printf("[名前]:%s\n", data.name);
    printf("[身長]:%f\n", data.stature);
    printf("[体重]:%f\n", data.weight);

    return;
}

冒頭でstudentという構造体名でテンプレートを用意しています。
構造体の中には構造体変数が6つ存在しており、それぞれ変数の型もまちまちです。

構造体のテンプレートの次に、void student_print(student data);として、
構造体を引数にした関数を宣言しています。
つまり、main関数の中でこの関数を呼び出すので、処理の中で使いますよ~。とコンパイラに教えているわけです。

main関数の冒頭で、構造体の実態として、studentという構造体テンプレートにて、dataという名前で生成しています。

構造体変数に値や文字列を格納していき、最後にstudent_print(data);として、構造体を引数として関数をcallしています。

この関数の中は、各構造体変数の値や文字列をprintf関数で表示するものです。つまり、main関数内で設定された値や文字列が正しく設定されているのか?を確認するだけの関数です。

参考書の通り、動作させた結果はmain関数で設定された値や文字列が正しく表示されているので、構造体としては引数で渡すことでデータも渡されていることが確認できた。という事例になります。

構造体でもポインタ変数

ポインタ自体は、格納値とアドレスをモードを切り替えてアクセスすることが可能であると解説しました。
ポインタの理解が難しくなるのは、ポインタ自体というよりは、ポインタと組み合わせて使われる媒体が出てくると、組み合わさる両者の知識が求められることにあります。

ここでは、構造体 + ポインタを解説しますが、この両者を把握していなければ、片側の知識だけでは理解が欠けてしまうことになりますので、少しずつ理解を深めてください。

#include <stdio.h>
#include <string.h>

typedef struct{
  int year;         /* 学生 */
  int clas;         /* クラス */
  int number;       /* 出席番号 */
  char name[64];    /* 名前 */
  double stateure;  /* 身長 */
  double weight;    /* 体重 */
}student;

int main(void)
{
    student data;
    student *pdata;

    /* 初期化 */
    pdata = &data;
    /* 通常変数モードへ切り替え */
    (*pdata).year = 10;
    /* 通常変数モードへ切り替え */
    strcpy((*pdata).name, "MARIO");

    return 0;
}

上記のプログラムは、通常の構造体と構造体のポインタ変数を用意しています。それが以下の部分です。

 student data;
student *pdata;

”初期化”とコメントされている以下にて、ポインタ変数に構造体dataのアドレスをセットしています。

  pdata = &data;

つまり、pdataはdataのアドレスを持ち、間接的にdataの構造体へアクセスできる状態になったということです。

アクセスできるようになったので、以下の2つの構造体変数へ値、文字列を設定しています。

 (*pdata).year = 10;
strcpy((*pdata).name, “MARIO”);

(*pdata).yearは、data.yearへアクセスしており、
(*pdata).nameは、data.nameへアクセスしています。

間接的にと書いたのは、pdataというポインタ変数を介して、
data構造体へ値をセットしたり、取得したりすることができるようになったからです。

ちなみに、(*pdata).name といった(*pdata)の書き方は通常しません。
構造体のポインタ変数を指定する時には、以下の様な書き方をするが一般的なので、覚えておいてください。

(*pdata).year は pdata->yearと書きます。
(*pdata).name は pdata->nameと書きます。

* の印をつけず、-> で書くことで、ポインタ変数が構造体変数にアクセスしていることが分かります。

->ですが、アロー演算子と呼びます。
また、- と > を 組み合わせていますので、IMEで「矢印」と入力して文字変換したものを採用しないでください。

構造体をポインタ引数にする

上記のコード例でポインタへ置き換えしたものが以下になります。
参考書と同じコード例になりますので、解説していきたいと思います。

処理の概要は以前と変わりません。
関数へ構造体の引数を渡していましたが、構造体ポインタをアドレスで渡すようにしたので & が付いています。
それと、関数内部はアロー演算子で構造体変数にアクセスするように変わっています。

#include <stdio.h>
#include <string.h>

typedef struct{
  int year;         /* 学生 */
  int clas;         /* クラス */
  int number;       /* 出席番号 */
  char name[64];    /* 名前 */
  double stateure;  /* 身長 */
  double weight;    /* 体重 */
}student;

void student_print(student *data);

int main(void)
{
    student data;

    data.year = 3;
    data.clas = 4;
    data.number = 18;
    strcpy(data.name,"MARIO");
    data.stature = 168.2;
    data.weight = 72.4;

    student_print(&data);  /* 構造体ポインタを引数とした関数にアドレスで呼び出し */

    return 0;
}

void student_print(student *data)
{
    printf("[学生]:%d\n", data->year);
    printf("[クラス]:%d\n", data->clas);
    printf("[出席番号]:%d\n", data->number);
    printf("[名前]:%s\n", data->name);
    printf("[身長]:%f\n", data->stature);
    printf("[体重]:%f\n", data->weight);

    return;
}

構造体を引数で渡せるのに、わざわざ構造体ポインタ変数として渡す理由はどこにあるでしょうか?
それは、関数の引数の時に解説しましたが、引数はコピーが渡されるコピーするのに処理時間がかかるという点にあります。
つまり、関数内部で渡された引数の中身を書き換えすることはできません。
でも、処理の都合上、関数の内部で書き換えしたい場合があります。

また、要素数が多いプログラムの場合、コピーに時間がかかり、該当関数が何度もcallされる処理なのであれば、callされる度に時間ロスを食らいます。

そんなケースの際に、ポインタ渡しをすると関数内部でポインタ変数を介して間接的に設定されたアドレス要素の値を書き換えすることが可能になり、処理時間も通常の変数を一つ渡すのと同じイメージになります。

故に、上記のコードでもstudent_print()関数内部でポインタ変数の構造体メンバの値を変更すると、main関数のdata構造体の変数の値は変わります。

設計上で注意が必要なのは、ポインタ変数での値書き換えを多用すると、変更されたくない要素が変更されたくないタイミングで変わっている。という不具合に遭遇する温床となります。

設計思想上、できるだけ変更すべきでない要素と仕方なく変更を許容する要素は、整理をした上で、プログラムコードとしても一目瞭然にしておくことが望ましいです。

構造体の配列

構造体も配列にすることができます。
以下は、構造体を配列として宣言した所までを書いたコードです。

typedef struct{
  int year;         /* 学生 */
  int clas;         /* クラス */
  int number;       /* 出席番号 */
  char name[64];    /* 名前 */
  double stateure;  /* 身長 */
  double weight;    /* 体重 */
}student;

int main(void
{
  student data[10];
}

studentという構造体名のテンプレートを持った構造体変数名dataが10個あるというものです。
アクセスの仕方も同じなので何も変わりません。
ただ、配列なので要素番号を指定する点が変わります。

 data[0].year;
data[1].year;
data[2].year;
data[3].year;

の様な感じです。
学生を想定した例になっていたので、生徒が100名いれば、data[100]みたく使うことが可能です。

参考書に記載されていますが、関数に構造体配列を引数とする関数の例を見てみましょう。

void student_print(student data[], int count)
{
   int i;
   for(i = 0; i < count; i++)
   {
       printf("[学生]:%d\n", data[i].year);
       printf("[クラス]:%d\n", data[i].clas);
       printf("[出席番号]:%d\n", data[i].number);
       printf("[名前]:%s\n", data[i].name);
       printf("[身長]:%f\n", data[i].stature);
       printf("[体重]:%f\n", data[i].weight);
   }

   return;
}

for文で配列要素数分だけ各配列要素の値や文字列を表示している。というのが処理の概要です。
引数として、student data[] を渡していますが、構造体ポインタでも言及した通り、コピーが渡されるので関数内での変更はできず、処理時間がかかるので扱う情報が多くなるのであればポインタでの処理が望ましいです。